#include "nim.h"

using namespace std;

int main(int argc, char ** argv) {

	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

	char hostName[NI_MAXHOST];
	int portNumber;

	if (argc == 1) {
		strcpy(hostName, DEFAULT_HOST);
		portNumber = DEFAULT_PORT;
	} else if (argc == 2) {
		strcpy(hostName, argv[1]);
		portNumber = DEFAULT_PORT;
	} else {
		strcpy(hostName, argv[1]);
		portNumber = atoi(argv[2]);

		if (portNumber <= 0 || portNumber > MAX_PORT_NUMBER) {
			cout << "Illegal port number!\n";
			exit(-1);
		}
	}

	clientTransmissions tranmissions;
	tranmissions.reciveTransmissions = 0;
	if (setupClient(tranmissions.clientSocket, portNumber, hostName) == -1) {
		cout << "Connection failed!\n";
	} else {
		startGame(tranmissions);
	}
	close(tranmissions.clientSocket);
	return 0;
}

/* connects to server */
int setupClient(int &sock, int portNumber, char *hostName) {
    sock = socket(PF_INET, SOCK_STREAM, 0);
	if (sock == -1) {
		return -1;
	}
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_port = htons(portNumber);
    struct addrinfo *addressInfo;
    if (getaddrinfo(hostName, NULL, NULL, &addressInfo) != 0) {
    	return -1;
    }
    struct sockaddr_in *result = (sockaddr_in*)((addressInfo->ai_addr));
    struct in_addr addressIn = result->sin_addr;
    freeaddrinfo(addressInfo);
    address.sin_addr = addressIn;
    int status = connect(sock, (struct sockaddr*)((&address)), sizeof (struct sockaddr));
    return status;
}

/* start game loop */
void startGame(clientTransmissions& transmissions) {
	bool quit = false;
	do {
		fd_set receiveFds;
		fd_set sendFds;
		FD_ZERO(&receiveFds);
		FD_ZERO(&sendFds);

		struct timeval timeout;
		timeout.tv_sec = SELECT_TIMEOUT;
		timeout.tv_usec = 0;

		// Receive:
		FD_SET(STDIN_SOCKET, &receiveFds);
		FD_SET(transmissions.clientSocket, &receiveFds);

		// Send:
		if (!transmissions.sendTransmissions.empty()) {
			FD_SET(transmissions.clientSocket, &sendFds);
		}

    	int status = select(transmissions.clientSocket+1, &receiveFds, &sendFds, NULL, &timeout);
    	if (status == -1) {
    		cout << "Disconnected from server\n";
    		break;
    	}

		if (FD_ISSET(transmissions.clientSocket, &sendFds)) {
			quit = sendTransmission(transmissions);
		}

		if (!quit && FD_ISSET(transmissions.clientSocket, &receiveFds)) {
			quit = receiveTransmission(transmissions);
		}

		if (!quit && FD_ISSET(STDIN_SOCKET, &receiveFds)) {
			quit = handleInputFromUser(transmissions);
		}
	} while (!quit);

	for (; !transmissions.sendTransmissions.empty(); delete transmissions.sendTransmissions.front(), transmissions.sendTransmissions.pop_front());
	if (transmissions.reciveTransmissions != 0) {
		delete transmissions.reciveTransmissions;
	}
}

bool sendTransmission(clientTransmissions& transmissions) {
	Transmission* transmission = transmissions.sendTransmissions.front();
	int sentBytes = send(transmissions.clientSocket, transmission->getBuffer() + transmission->getCompleteBytes(), transmission->getRemainingBytes(), 0);
	if (sentBytes == -1) {
		cout << "Disconnected from server\n";
		return true;
	}
	transmission->incCompleteBytes(sentBytes);
	if (transmission->isComplete()) {
		transmissions.sendTransmissions.pop_front();
		delete transmission;
	}
	return false;
}

bool receiveTransmission(clientTransmissions& transmissions) {
	Transmission*& transmission = transmissions.reciveTransmissions;
	if (!transmission) {
		byte opcode;
		int status = recv(transmissions.clientSocket, &opcode, 1, 0);
		if (status == -1 || status == 0) {
			cout << "Disconnected from server\n";
			return true;
		}
		bool isMsgOpcode = opcode == OPCODE_SERVER_MESSAGE;
		transmission = new Transmission(opcode, isMsgOpcode);
		if (!isMsgOpcode) {
			transmission->setTotalBytes(getMessageLengthByOpCode(opcode));
		}
		if (transmission->isComplete()) {
			bool quit = handleIncomingTransmission(*transmission, transmissions);
			delete transmission;
			transmissions.reciveTransmissions = 0;
			return quit;
		}
	}

	if (transmission->isOngoingTransmission()) {
		int received = recv(transmissions.clientSocket, transmission->getBuffer() + transmission->getCompleteBytes(), transmission->getRemainingBytes(), 0);
		if (received == -1 || received == 0) {
			cout << "Disconnected from server\n";
			return true;
		}
		transmission->incCompleteBytes(received);
	} else {
		// This case is only for client chat message when we still don't know the total length:
		int msgLength = getMessageLengthByOpCode(OPCODE_SERVER_MESSAGE);
		int remainingLength = msgLength - transmission->getCompleteBytes();
		int received = recv(transmissions.clientSocket, transmission->getBuffer() + transmission->getCompleteBytes(), remainingLength, 0);
		if (received == -1 || received == 0) {
			cout << "Disconnected from server\n";
			return true;
		}
		transmission->incCompleteBytes(received);

		if (received == remainingLength) { // Finished receiving message length
			int32_t totalBytes;
			memcpy(&totalBytes,transmission->getBuffer()+1,sizeof(int32_t));
			transmission->setTotalBytes(totalBytes + msgLength);
		}
	}

	if (transmission->isComplete()) {
		bool quit = handleIncomingTransmission(*transmission, transmissions);
		delete transmission;
		transmissions.reciveTransmissions = 0;
		return quit;
	}
	return false;
}

/* calls the suitable handler function by opcode */
bool handleIncomingTransmission(Transmission& transmissionToHandle, clientTransmissions& transmissions) {
	switch (transmissionToHandle.getOpcode()) {
	case OPCODE_STATUS:
		handleStatusOpcode(transmissionToHandle.getBuffer());
		return false;
	case OPCODE_ACK:
		handleAckOpcode(transmissionToHandle.getBuffer());
		return false;
	case OPCODE_GAME_TYPE:
		handleGameTypeOpcode(transmissionToHandle.getBuffer());
		return false;
	case OPCODE_GAME_END_STATUS:
		handleGameEndOpcode(transmissionToHandle.getBuffer());
		return true;
	case OPCODE_SERVER_MESSAGE:
		handleChatMessageOpcode(transmissionToHandle.getBuffer());
		return false;
	case OPCODE_YOUR_TURN:
		handleYouTurnOpcode();
		return false;
	case OPCODE_PLAYER_STATUS:
		handleConnectedOpcode(transmissionToHandle.getBuffer());
		return false;
	case OPCODE_PLAYER_GAME_STATUS:
		handleClientStatusOpcode(transmissionToHandle.getBuffer());
		return false;
	case OPCODE_ILLEGAL_NOT_IN_TURN_MESSAGE:
		handleIllegalMoveOpcode();
		return false;
	case OPCODE_CLIENT_REJECTED:
		handleClientRejectedOpcode();
		return true;
	default:
		cout << "Illegal opcode!\n";
		return true;
	}
}

/* receives input from user and create suitable transmission */
bool handleInputFromUser(clientTransmissions& transmissions) {
	string input;
	getline(cin,input);

	string prefix = "MSG ";
	if (input.size() > prefix.size()+1 && !input.compare(0, prefix.size(), prefix)) {
		size_t found = input.find_first_of(' ', prefix.size());
		if (found != string::npos) {
			int val;
			istringstream stream(input.substr(prefix.size(),found));
			if (stream>>val) {
				byte dst = (byte)val;
				string msg = input.substr(found+1,input.size());

				if (OPCODE_CLIENT_MESSAGE_HEADER_LENGTH + msg.size() > MAX_MESSAGE_SIZE) {
					// truncating message to fix buffer
					msg = msg.substr(0,MAX_MESSAGE_SIZE-OPCODE_CLIENT_MESSAGE_HEADER_LENGTH);
				}

				createChatMessageOpcode(transmissions,dst,msg);
				return false;
			}
		}
		createIllegalMoveOpcode(transmissions);
		return false;
	}

	if (input.size() > 0) {
		char stack = input[0];
		if (stack == 'Q') {
			if (input.size() == 1) {
				createQuitOpcode(transmissions.clientSocket);
				return true;
			} else {
				createIllegalMoveOpcode(transmissions);
				return false;
			}
		}

		if (input.size() >= 3) {
			int32_t amount;
			istringstream stream(input.substr(2,input.size()));
			if (stream>>amount && amount >= 0 && stack >= 'A' && stack <= 'C') {
				ostringstream ostream;
				ostream << amount;
				unsigned int amountSize = ostream.str().size();

				if (input.size() > 2 + amountSize) {
					createIllegalMoveOpcode(transmissions);
					return false;
				}

				createMoveOpcode(transmissions,stack,amount);
				return false;
			}
		}
	}

	createIllegalMoveOpcode(transmissions);
	return false;
}

/* create illegal move transmission */
void createIllegalMoveOpcode(clientTransmissions& transmissions) {
	Transmission *transmission = new Transmission(OPCODE_CLIENT_ILLEGAL_MOVE,false);
	transmission->getBuffer()[0] = OPCODE_CLIENT_ILLEGAL_MOVE;
	transmission->setTotalBytes(OPCODE_CLIENT_ILLEGAL_MOVE_LENGTH);
	transmissions.sendTransmissions.push_back(transmission);
}

/* sends user's input back to server */
void createMoveOpcode(clientTransmissions& transmissions, char stack, int32_t amount) {
	Transmission *transmission = new Transmission(OPCODE_CLIENT_MOVE,false);
	transmission->getBuffer()[0] = OPCODE_CLIENT_MOVE;
	transmission->getBuffer()[1] = stack;
	memcpy(transmission->getBuffer()+2, &amount, sizeof(int32_t));
	transmission->setTotalBytes(OPCODE_CLIENT_MOVE_LENGTH);
	transmissions.sendTransmissions.push_back(transmission);
}

/* create chat message transmission */
void createChatMessageOpcode(clientTransmissions& transmissions, byte dst, string& msg) {
	Transmission *transmission = new Transmission(OPCODE_CLIENT_MESSAGE,true);
	transmission->getBuffer()[0] = OPCODE_CLIENT_MESSAGE;
	transmission->getBuffer()[1] = dst;
	int32_t msgLength = msg.length();
	memcpy(transmission->getBuffer()+2, &msgLength, sizeof(int32_t));
	msg.copy(transmission->getBuffer()+OPCODE_CLIENT_MESSAGE_HEADER_LENGTH, msgLength, 0);
	transmission->setTotalBytes(OPCODE_CLIENT_MESSAGE_HEADER_LENGTH + msgLength);
	transmissions.sendTransmissions.push_back(transmission);
}

/* send quit opcode to server */
void createQuitOpcode(int &sock) {
	byte opcode = OPCODE_CLIENT_QUIT;
	safeSend(sock, &opcode, OPCODE_CLIENT_QUIT_LENGTH);
	cout << "Disconnected from server\n";
}

/* prints your turn message */
inline void handleYouTurnOpcode() {
	cout << "Your turn:\n";
}

/* prints heaps status message */
void handleStatusOpcode(char *buffer) {
	int32_t stacks[NUMBER_OF_HEAPS];
	memcpy(stacks, buffer, NUMBER_OF_HEAPS*sizeof(int32_t));

	//prints heaps status message:
	cout << "Heap sizes are " << stacks[0];
	for (int i = 1; i < NUMBER_OF_HEAPS; ++i) {
		cout << ", " << stacks[i];
	}
	cout << "\n";
}

/* prints legal/illegal move message */
void handleAckOpcode(char *buffer) {
	char isLegalMove = buffer[0];
	if (isLegalMove) {
		cout << "Move accepted\n";
	} else {
		cout << "Illegal move\n";
	}
}

/* prints game type message */
void handleGameTypeOpcode(char *buffer) {
	char isMisere = buffer[0];
	if (isMisere) {
		cout << "This is a Misere game\n";
	} else {
		cout << "This is a Regular game\n";
	}
}

/* prints game ended message */
void handleGameEndOpcode(char *buffer) {
	char gameEndStatus = buffer[0];
	if (gameEndStatus == 0) {
		cout << "You lose!\n";
	} else if (gameEndStatus == 1) {
		cout << "You win!\n";
	} else {
		cout << "Game over!\n";
	}
}

/* prints client status message */
void handleClientStatusOpcode(char *buffer) {
	char status = buffer[0];
	if (status == 0) {
		cout << "You are only viewing\n";
	} else if (status == 1) {
		cout << "You are playing\n";
	} else {
		cout << "You are now playing!\n";
	}
}

/* prints number of players in server and this client number */
void handleConnectedOpcode(char *buffer) {
	int32_t numOfPlayers = (int32_t)buffer[0];
	int32_t userID = (int32_t)buffer[1];
	cout << "Number of players is " << numOfPlayers << "\n";
	cout << "You are client " << userID << "\n";
}

/* prints chat message */
void handleChatMessageOpcode(char *buffer) {
	byte src = buffer[0];
	int32_t msgLength;
	memcpy(&msgLength, buffer+1, sizeof(int32_t));
	buffer[1 + sizeof(int32_t) + msgLength] = '\0';
	cout << (int)src << ": " << (buffer + 1 + sizeof(int32_t)) << "\n";
}

/* prints client rejected message */
inline void handleClientRejectedOpcode() {
	cout << "Client rejected: too many clients are already connected\n";
}

/* prints not in turn move message */
inline void handleIllegalMoveOpcode() {
	cout << "Move rejected: this is not your turn\n";
}
